//
//  TTDatabase.m
//  Tint
//
//  Created by Kevin Wu on 2/28/12.
//  Copyright (c) 2012 Telligenty. All rights reserved.
//

#import "TTDatabase.h"


@interface TTDatabase (Private)

- (BOOL)bindStatement:(sqlite3_stmt *)statement toParameters:(NSArray *)parameters;
- (void)bindObject:(id)object toColumn:(int)index inStatement:(sqlite3_stmt *)statement;

@end

@implementation TTDatabase (Private)

- (BOOL)bindStatement:(sqlite3_stmt *)statement toParameters:(NSArray *)parameters
{
	int queryCount = sqlite3_bind_parameter_count(statement);
    for ( int i=0; i<[parameters count]; ++i ) {
        id object = [parameters objectAtIndex:i];
        [self bindObject:object toColumn:i+1 inStatement:statement];
    }
	return ( [parameters count] == queryCount );
}

/*
 * NSNull * -> NULL
 * NSNumber *
 *   BOOL -> INTEGER
 *   int -> INTEGER
 *   long long -> INTEGER
 *   double -> REAL
 * NSString * -> TEXT
 * NSDate * -> TEXT
 * NSData * -> BLOB (not support)
 */
- (void)bindObject:(id)object toColumn:(int)index inStatement:(sqlite3_stmt *)statement
{
    if ( (object == nil) || ((NSNull *)object == [NSNull null]) ) {
		sqlite3_bind_null(statement, index);
	} else if ( [object isKindOfClass:[NSNumber class]] ) {
        if ( strcmp([object objCType], @encode(BOOL)) == 0 ) {
			sqlite3_bind_int(statement, index, ([object boolValue] ? 1 : 0));
		} else if ( strcmp([object objCType], @encode(int)) == 0 ) {
			sqlite3_bind_int64(statement, index, [object intValue]);
		} else if ( strcmp([object objCType], @encode(long long)) == 0 ) {
			sqlite3_bind_int64(statement, index, [object longLongValue]);
		} else if ( strcmp([object objCType], @encode(double)) == 0 ) {
			sqlite3_bind_double(statement, index, [object doubleValue]);
		} else {
			sqlite3_bind_text(statement, index, [[object description] UTF8String], -1, SQLITE_STATIC);
		}
    } else if ( [object isKindOfClass:[NSString class]] ) {
        sqlite3_bind_text(statement, index, [object UTF8String], -1, SQLITE_STATIC);
    } else if ( [object isKindOfClass:[NSDate class]] ) {
        sqlite3_bind_text(statement, index, [TTRFC3339DateStringFromDate(object) UTF8String], -1, SQLITE_STATIC);
    } else {
		sqlite3_bind_text(statement, index, [[object description] UTF8String], -1, SQLITE_STATIC);
	}
}

@end



@implementation TTDatabase


#pragma mark - Accessors

@synthesize handle = _handle;
@synthesize path = _path;



#pragma mark - Memory

- (id)initWithPath:(NSString *)path
{
    self = [super init];
    if ( self ) {
        _handle = NULL;
        _path = [[NSString alloc] initWithString:path];
        
        _opened = NO;
        
        _executeLock = [[NSLock alloc] init];
    }
    return self;
}

- (void)dealloc
{
    [self close];
    
    _handle = NULL;
    TTRelease(_path);
    
    _opened = NO;
    
    TTRelease(_executeLock);
    [super dealloc];
}



#pragma mark - Singleton

static TTDatabase *sharedDatabase = nil;

+ (TTDatabase *)sharedObject
{
    return sharedDatabase;
}

+ (void)setSharedObject:(TTDatabase *)object
{
    TTRetain(object, sharedDatabase);
}


#pragma mark - Sqlite status

- (BOOL)open
{
	if ( _opened ) {
        return YES;
    }
	
	int returnCode = sqlite3_open([_path fileSystemRepresentation], &_handle);
	if ( returnCode != SQLITE_OK ) {
        return NO;
    }
	
	_opened = YES;
	return YES;
}

- (void)close
{
	if ( _handle ) {
        sqlite3_close(_handle);
        _opened = NO;
    }
}

- (BOOL)goodConnection
{
    if ( _handle ) {
        NSString *sql = @"SELECT * FROM sqlite_master WHERE type='table';";
        TTDatabaseResult *result = [self newResultByExecutingQueryString:sql];
        BOOL status = ( result != nil );
        TTRelease(result);
        return status;
    }
    return NO;
}

- (BOOL)hasTable:(NSString *)tableName
{
    if ( _handle && tableName ) {
        NSString *sql = @"SELECT COUNT(*) AS count FROM sqlite_master WHERE type='table' AND name=?;";
        TTDatabaseResult *result = [self newResultByExecutingQuery:sql, tableName, nil];
        
        TTDatabaseRow *row = [result rowAtIndex:0];
        NSUInteger count = [row intForColumnAtIndex:0];
        TTRelease(result);
        
        return ( count > 0 );
    }
    return NO;
}

- (NSUInteger)fileSize
{
    NSError *error = nil;
    NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:_path error:&error];
    if ( error == nil ) {
        return [attributes fileSize];
    }
    return 0;
}



#pragma mark - Execute update

- (BOOL)executeUpdateString:(NSString *)sql
{
    return [self executeUpdate:sql, nil];
}

- (BOOL)executeUpdate:(NSString *)sql, ...
{
    NSMutableArray *parameters = nil;
    TTValistToArray(sql, parameters);
    BOOL status = [self executeUpdate:sql parameters:parameters];
    TTRelease(parameters);
    return status;
}

- (BOOL)executeUpdate:(NSString *)sql parameters:(NSArray *)parameters
{
    [_executeLock lock];
    
	if ( [self open] ) {
        
        int returnCode = 0;
        sqlite3_stmt *statement = NULL;
        
        returnCode = sqlite3_prepare(_handle, [sql UTF8String], -1, &statement, 0);
        if ( returnCode == SQLITE_BUSY ) {
            TTDPRINT(@"[Update] Database busy while preparing: %@", sql);
            [_executeLock unlock];
            return NO;
        } else if ( returnCode != SQLITE_OK ) {
            TTDPRINT(@"[Update] Query failed while preparing: %@", sql);
            sqlite3_finalize(statement);
            [_executeLock unlock];
            return NO;
        }
        
        if ( ![self bindStatement:statement toParameters:parameters] ) {
            TTDPRINT(@"[Update] Bind failed: %@", sql);
            sqlite3_finalize(statement);
            [_executeLock unlock];
            return NO;
        }
        
        returnCode = sqlite3_step(statement);
        if ( SQLITE_BUSY == returnCode ) {
            TTDPRINT(@"[Update] Database Busy while stepping: %@", sql);
        } else if ( (SQLITE_DONE == returnCode) || (SQLITE_ROW == returnCode) ) {
            
        } else if ( SQLITE_ERROR == returnCode ) {
            TTDPRINT(@"[Update] SQLITE_ERROR  while stepping: %@", sql);
        } else if ( SQLITE_MISUSE == returnCode ) {
            TTDPRINT(@"[Update] SQLITE_MISUSE  while stepping: %@", sql);
        } else {
            TTDPRINT(@"[Update] UNKNOWN_ERROR  while stepping: %@", sql);
        }
        
        returnCode = sqlite3_finalize(statement);
        
        [_executeLock unlock];
        
        return ( returnCode == SQLITE_OK );
        
	}
    
    [_executeLock unlock];
    return NO;
}



#pragma mark - Execute insert

- (sqlite3_int64)executeInsertString:(NSString *)sql
{
    return [self executeInsert:sql, nil];
}

- (sqlite3_int64)executeInsert:(NSString *)sql, ...
{
    NSMutableArray *parameters = nil;
    TTValistToArray(sql, parameters);
    sqlite3_int64 rowid = [self executeInsert:sql parameters:parameters];
    TTRelease(parameters);
    return rowid;
}

- (sqlite3_int64)executeInsert:(NSString *)sql parameters:(NSArray *)parameters
{
    [_executeLock lock];
    
	if ( [self open] ) {
		
        int returnCode = 0;
        sqlite3_stmt *statement = NULL;
        
        returnCode = sqlite3_prepare(_handle, [sql UTF8String], -1, &statement, 0);
        if ( returnCode == SQLITE_BUSY ) {
            TTDPRINT(@"[Insert] Database busy while preparing: %@", sql);
            [_executeLock unlock];
            return 0;
        } else if ( returnCode != SQLITE_OK ) {
            TTDPRINT(@"[Insert] Query failed while preparing: %@", sql);
            sqlite3_finalize(statement);
            [_executeLock unlock];
            return 0;
        }
        
        if ( ![self bindStatement:statement toParameters:parameters] ) {
            TTDPRINT(@"[Insert] Bind failed: %@", sql);
            sqlite3_finalize(statement);
            [_executeLock unlock];
            return 0;
        }
        
        returnCode = sqlite3_step(statement);
        if ( SQLITE_BUSY == returnCode ) {
            TTDPRINT(@"[Insert] Database Busy while stepping: %@", sql);
        } else if ( (SQLITE_DONE == returnCode) || (SQLITE_ROW == returnCode) ) {
            
        } else if ( SQLITE_ERROR == returnCode ) {
            TTDPRINT(@"[Insert] SQLITE_ERROR while stepping: %@", sql);
        } else if ( SQLITE_MISUSE == returnCode ) {
            TTDPRINT(@"[Insert] SQLITE_MISUSE while stepping: %@", sql);
        } else {
            TTDPRINT(@"[Insert] UNKNOWN_ERROR while stepping: %@", sql);
        }
        
        returnCode = sqlite3_finalize(statement);
        sqlite3_int64 rowid = sqlite3_last_insert_rowid(_handle);
        
        [_executeLock unlock];
        
        if ( returnCode == SQLITE_OK ) {
            return rowid;
        }
        
        return 0;
        
	}
    
    [_executeLock unlock];
    return 0;
}



#pragma mark - Execute query

- (TTDatabaseResult *)newResultByExecutingQueryString:(NSString *)sql
{
    return [self newResultByExecutingQuery:sql, nil];
}

- (TTDatabaseResult *)newResultByExecutingQuery:(NSString *)sql, ...
{
    NSMutableArray *parameters = nil;
    TTValistToArray(sql, parameters);
    TTDatabaseResult *result = [self newResultByExecutingQuery:sql parameters:parameters];
    TTRelease(parameters);
    return result;
}

- (TTDatabaseResult *)newResultByExecutingQuery:(NSString *)sql parameters:(NSArray *)parameters
{
	[_executeLock lock];
    
    if ( [self open] ) {
        
        int returnCode = 0;
        sqlite3_stmt *statement = NULL;
        
        returnCode = sqlite3_prepare(_handle, [sql UTF8String], -1, &statement, 0);
        if ( returnCode == SQLITE_BUSY ) {
            TTDPRINT(@"[Query] Database busy while preparing: %@", sql);
            [_executeLock unlock];
            return nil;
        } else if ( returnCode != SQLITE_OK ) {
            TTDPRINT(@"[Query] Query failed while preparing: %@", sql);
            sqlite3_finalize(statement);
            [_executeLock unlock];
            return nil;
        }
        
        if ( ![self bindStatement:statement toParameters:parameters] ) {
            TTDPRINT(@"[Query] Bind failed: %@", sql);
            sqlite3_finalize(statement);
            [_executeLock unlock];
            return nil;
        }
        
        TTDatabaseResult *result = [[TTDatabaseResult alloc] init];
        result.errorCode = [self lastErrorCode];
        result.errorMessage = [self lastErrorMessage];
        
        int columnCount = sqlite3_column_count(statement);
        for ( int i=0; i<columnCount; i++ ) {
            if ( sqlite3_column_name(statement, i) ) {
                NSString *value = [[NSString alloc] initWithUTF8String:sqlite3_column_name(statement, i)];
                [result.columnNames addObject:value];
                [value release];
            } else {
                NSString *value = [[NSString alloc] initWithFormat:@"%d", i];
                [result.columnNames addObject:value];
                [value release];
            }
            
            if ( sqlite3_column_decltype(statement, i) ) {
                NSString *value = [[NSString alloc] initWithUTF8String:sqlite3_column_decltype(statement, i)];
                [result.columnTypes addObject:value];
                [value release];
            } else {
                NSString *value = [[NSString alloc] init];
                [result.columnTypes addObject:value];
                [value release];
            }
        }
        
        while ( sqlite3_step(statement) == SQLITE_ROW ) {
            TTDatabaseRow *row = [[TTDatabaseRow alloc] initWithDatabaseResult:result];
            for ( int i=0; i<columnCount; i++ ) {
                if ( sqlite3_column_text(statement, i) ) {
                    NSString *value = [[NSString alloc] initWithUTF8String:(const char *)sqlite3_column_text(statement, i)];
                    [row.columnData addObject:value];
                    [value release];
                } else {
                    NSString *value = [[NSString alloc] init];
                    [row.columnData addObject:value];
                    [value release];
                }
            }
            [result addRow:row];
            TTRelease(row);
        }
        
        sqlite3_finalize(statement);
        
        [_executeLock unlock];
        
        if ( [result count] < 1 ) {
            TTRelease(result);
        }
        
        return result;
        
	}
    
    [_executeLock unlock];
    return nil;
}


#pragma mark - Error methods

- (NSString *)lastErrorMessage
{
    return [NSString stringWithUTF8String:sqlite3_errmsg(_handle)];
}

- (int)lastErrorCode
{
    return sqlite3_errcode(_handle);
}

- (BOOL)hadError
{
    int lastErrCode = [self lastErrorCode];
    return ((lastErrCode > SQLITE_OK) && 
            (lastErrCode < SQLITE_ROW)
            );
}



#pragma mark - Sqlite ability

+ (BOOL)isSQLiteThreadSafe
{
    // Make sure to read the sqlite headers on this guy!
    return sqlite3_threadsafe();
}

+ (NSString *)sqliteVersion
{
    return [NSString stringWithFormat:@"%s", sqlite3_libversion()];
}

@end

